# Este código é apenas uma reprodução do script original.from sklearn.base import BaseEstimator, TransformerMixinfrom typing import Unionimport pandas as pdimport numpy as npclass DropConstantColumns(BaseEstimator, TransformerMixin):""" This class is made to work as a step in sklearn.pipeline.Pipeline object. It drops constant columns from a pandas dataframe object. Important: the constant columns are found in the fit function and dropped in the transform function. """def__init__(self, print_cols: bool=False, thresh: float=None, search: Union[float, int] =None, ignore_prefix: list[str] = [], also: list[str] = []) ->None:""" print_cols: default = False. Determine whether the fit function should print the constant columns' names. thresh: default = None. If any value occurs more than this fraction of the total number of rows, the column is considered constant. Initiates the class. """self.print_cols = print_colsself.also = alsoself.thresh = threshself.ignore_prefix = ignore_prefixself.search = searchpassdef fit(self, X: pd.DataFrame , y: None=None) ->None:""" X: dataset whose constant columns should be removed. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the names of the columns to be removed in the transform function. """ifself.thresh isNone:self.constant_cols = [ colfor col in X.columnsif ( ((X[col].nunique() ==1) | (col inself.also))&~any([col.startswith(prefix) for prefix inself.ignore_prefix]) ) ]elifself.search isNone:self.constant_cols = [ colfor col in X.columnsif ( (X[col].value_counts(normalize=True).max() >self.thresh)&~any([col.startswith(prefix) for prefix inself.ignore_prefix]) ) ]else:self.constant_cols = [ colfor col in X.columnsif ( ((X[col]==self.search).sum()/X.shape[0] >self.thresh)& (~any([col.startswith(prefix) for prefix inself.ignore_prefix])) ) ]ifself.print_cols:print(f"{len(self.constant_cols)} constant columns were found.")returnselfdef transform(self, X: pd.DataFrame) -> pd.DataFrame:""" X: dataset whose constant columns should be removed. Returns dataset without the constant columns found in the fit function. """return X.copy().drop(self.constant_cols, axis=1)class DropDuplicateColumns(BaseEstimator, TransformerMixin):""" This class is made to work as a step in sklearn.pipeline.Pipeline object. It drops duplicate columns from a pandas dataframe object. Important: the duplicate columns are found in the fit function and dropped in the transform function. """def__init__(self, print_cols: bool=False, ignore: list[str] = []) ->None:""" print_cols: default = False. Determine whether the fit function should print the duplicate columns' names. ignore: list of columns to ignore. Initiates the class. """self.print_cols = print_colsself.ignore = ignorepassdef fit(self, X: pd.DataFrame, y: None=None) ->None:""" X: dataset whose duplicate columns should be removed. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the names of the columns to be removed in the transform function. """ regular_columns = [] duplicate_columns = [] sorted_cols =sorted(X.columns)for col0 in sorted_cols:if col0 notin duplicate_columns: regular_columns.append(col0)for col1 in sorted_cols:if (col0 != col1):if X[col0].equals(X[col1]):if col1 notin regular_columns: duplicate_columns.append(col1)self.duplicate_cols = duplicate_columnsifself.print_cols:print(f"{len(duplicate_columns)} duplicate columns were found.")returnselfdef transform(self, X: pd.DataFrame) -> pd.DataFrame:""" X: dataset whose duplicate columns should be removed. Returns dataset without the duplicate columns found in the fit function. """ X_ = X.copy()return X_.drop(self.duplicate_cols, axis=1)class AddNonZeroCount(BaseEstimator, TransformerMixin):""" This class is made to work as a step in sklearn.pipeline.Pipeline object. """def__init__(self, prefix: str="", ignore: list[str] = []) ->None:""" prefix: prefix of the columns to be summed. ignore: list of columns to ignore. fake_value: value to be replaced with None. Initiates de class. """self.prefix = prefixself.ignore = ignorepassdef fit(self, X: pd.DataFrame, y: None=None) ->None:""" X: dataset whose "prefix" variables different than 0 should be counted. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the names of the columns whose not 0 values should be counted in the transform function. """self.prefix_cols = [ colfor col in X.columnsif ( (col.startswith(self.prefix))& (col notinself.ignore) ) ]returnselfdef transform(self, X: pd.DataFrame) -> pd.DataFrame:""" X: dataset whose "prefix" variables' not 0 values should be counted. Returns dataset with new column with the count of the "prefix" variables' not 0 values. """ X_ = X.copy() X_[f"non_zero_count_{self.prefix}"] = X_[self.prefix_cols] \ .applymap(lambda x: 0if ((x ==0) | (x ==None)) else1) \ .sum(axis=1)return X_class CustomSum(BaseEstimator, TransformerMixin):""" This class is made to work as a step in sklearn.pipeline.Pipeline object. It sums columns from a pandas dataframe object based on the columns prefix. """def__init__(self, prefix: str="", ignore: list[str] = []) ->None:""" prefix: prefix of the columns to be summed. ignore: list of columns to ignore. fake_value: value to be replaced with None. Initiates de class. """self.prefix = prefixself.ignore = ignorepassdef fit(self, X: pd.DataFrame, y: None=None) ->None:""" X: dataset whose columns with "prefix" should be summed. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the names of the columns to be summed in the transform function. """self.prefix_cols = [ colfor col in X.columnsif ( (col.startswith(self.prefix))& (col notinself.ignore) ) ]returnselfdef transform(self, X: pd.DataFrame) -> pd.DataFrame:""" X: dataset whose "prefix" variables should be summed. Returns dataset with new column with the sum of the "prefix" variables. """ X_ = X.copy() X_[f"sum_of_{self.prefix}"] = X_[self.prefix_cols] \ .sum(axis=1)return X_class CustomImputer(BaseEstimator, TransformerMixin):""" This class is made to work as a step in a sklearn.pipeline.Pipeline object. It imputes values in a pandas dataframe object based on the columns prefix. """def__init__(self, prefix: str, to_replace: Union[int, float, str], replace_with: Union[int, float, str] = np.nan, ignore: list[str] = []) ->None:""" prefix: prefix of the columns to be imputed. to_replace: value to be replaced. replace_with: value to replace "to_replace" with. ignore: list of columns to ignore. Initiates de class. """self.prefix = prefixself.to_replace = to_replaceself.replace_with = replace_withself.ignore = ignorepassdef fit(self, X: Union[pd.DataFrame, pd.Series], y: None=None) ->None:""" X: dataset whose columns with "prefix" should be imputed. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the names of the columns to be imputed in the transform function. """self.prefix_cols = [ colfor col in X.columnsif ( (col.startswith(self.prefix))& (col notinself.ignore) ) ]returnselfdef transform(self, X: Union[pd.DataFrame, pd.Series]) -> Union[pd.DataFrame, pd.Series]:""" X: dataset whose columns with "prefix" should be imputed. Returns dataset with the imputed columns. """ X_ = X.copy() X_[self.prefix_cols] = X_[self.prefix_cols] \ .replace(self.to_replace, self.replace_with)return X_class AddNoneCount(BaseEstimator, TransformerMixin):""" This class is made to work as a step in sklearn.pipeline.Pipeline object. It counts the number of None values in a pandas dataframe object based on the columns prefix. """def__init__(self, prefix: str="", ignore: list[str] = []) ->None:""" prefix: subset of variables for none count starting with this string. fake_value: values inserted to replace None. ignore: list of columns with prefix to ignore. drop_constant: whether to drop columns that would become constant without missing features or not. """self.prefix = prefixself.ignore = ignorepassdef fit(self, X: pd.DataFrame, y: None=None) ->None:""" X: dataset whose "prefix" variables' null values should be counted. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the names of the columns whose null values should be counted in the transform function. """self.prefix_cols = [ colfor col in X.columnsif ( (col.startswith(self.prefix))& (col notinself.ignore) ) ]returnselfdef transform(self, X: pd.DataFrame) -> pd.DataFrame:""" X: dataset to apply transformation on. Returns dataset with new column with the count of the "prefix" variables' null values. """ X_ = X.copy() X_[f"none_count_{self.prefix}"] = X_[self.prefix_cols] \ .isnull() \ .sum(axis=1)return X_class CustomEncoder(BaseEstimator, TransformerMixin):""" This class is made to work as a step in sklearn.pipeline.Pipeline object. It encodes categorical variables in a pandas dataframe based on the categories mean of the target variable. Unknown values must be defined by the user. """def__init__(self, colname: str) ->None:""" labels: dictionary with the labels to be replaced. colname: name of the column to be encoded. Initiates de class. """self.colname = colnamepassdef fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, pd.Series]) ->None:""" X: dataset whose column should be encoded. y: Shouldn't be used. Only exists to prevent raise Exception due to accidental input in a pipeline. Creates class atributte with the dictionary to be used in the transform function. """ X_ = X.copy().assign(TARGET=y) grouped_X_ = X_ \ .groupby(self.colname) \ .agg({"TARGET": "mean"}) \ .sort_values("TARGET", ascending=True) groups = grouped_X_.indexself.labels ={ groups[i]: ifor i inrange(len(groups)) }self.most_frequent = X_[self.colname].mode()[0]returnselfdef _apply_map(self, x: Union[int, str]) ->int:""" x: value to be replaced. Returns the value to replace "x" with. """if x inself.labels.keys():returnself.labels[x]else:returnself.labels[self.most_frequent]def transform(self, X: pd.DataFrame) -> pd.DataFrame:""" X: dataset whose column should be encoded. Returns dataset with the encoded column. """ X_ = X.copy() X_[self.colname] = X_[self.colname] \ .apply(self._apply_map)return X_class CustomLog(BaseEstimator, TransformerMixin):def__init__(self, columns: list[str] = []) ->None:self.columns = columnspassdef fit(self, X, y=None):returnselfdef transform(self, X): X_ = X.copy() X_[self.columns] = np.log1p( X_[self.columns] - X_[self.columns].min() )return X_
Distribuições parecidas, mas com centros ligeiramente distantes. Não foram realizados testes de hipótese para validar a significância estatística deste (e de outros) deslocamento(s), o que poderia ser um ponto de melhoria no estudo.
Classes pouco frequentes: abordagem diferente para cada algorítmo. Nenhuma classe de proporção de insatisfeitos maior que a da classe mais frequente é populosa o suficiente nem tem proporção de insatisfeitos tão maior que a da classe mais frequente para ser considerada relevante.
Outra variável que possui valores faltantes (-999999) e que também não parece ter poder de discriminação. Como lidar com estes valores varia entre o algorítmo implementado, como discutido anteriormente e no notebook de origem do código abaixo (seção de delta).
Esta variável parece contínua mas tem apenas valores inteiros. Sua distribuição é bem diferente entre as classes da variável de interesse, podendo ser uma boa preditora.
Mostrar/esconder código
col ="var15"cv.binaryhistplot(df[col], df["TARGET"], nbins=20)
Variável numérica com forte assimetria à direita. Seu poder de predição não parece tão forte quanto o de var15, mas pode ser muito útil na análise de clusters por ter boa variabilidade.
Mostrar/esconder código
var ="var38"cv.binaryhistplot( df[var].apply(np.log1p), df["TARGET"], nbins=20)
Uma função recebe o conjunto de treino, um pipeline de classificação e uma malha de hiperparâmetros, executa a sequência de fit do wrapper para então serializar e armazenar a instância da classe com pickle para uso posterior.
Sequência de fit:
Separa o conjunto de treino entre treino e validação
Treina o pipeline com GridSearchCV no conjunto de treino maximizando a AUC
Ajusta o corte de classificação no conjunto de validação maximizando o lucro total (com os valores propostos no case)
Retreina o modelo com os melhores hiperparâmetros no conjunto de treino completo
Avaliação:
Já treinado, recebe o conjunto de teste
Classifica o conjunto de teste
Calcula métricas de negócios
Calcula métricas de classificação
Feature Importances
Ainda treinado, recebe o conjunto de teste
Classifica o conjunto de teste fazendo shuffle em cada variável de interesse diversas vezes
Para cada variável, calcula a diferença média na métrica de lucro total entre o conjunto original e o conjunto com a variável embaralhada
Ordena as variáveis pela diferença média e retorna o resultado em um DataFrame
Rankeamento:
Ainda treinado, recebe o conjunto de teste
Classifica o conjunto de testes usando quocientes do corte de classificação
Mostrar/esconder código
# Este código é apenas uma reprodução do script original.# --- Function Building --- #from typing import Union# --- Threshold Optimization --- #from scipy.optimize import minimize_scalar# --- Data Manipulation --- #import numpy as npimport pandas as pd# --- sklearn utils --- #from sklearn.pipeline import Pipelinefrom sklearn.model_selection import\ train_test_split, \ GridSearchCV, \ StratifiedKFold# --- sklearn metrics --- #from sklearn.inspection import permutation_importancefrom sklearn.metrics import\ roc_auc_score, \ confusion_matrix, \ accuracy_score, \ precision_score, \ recall_score, \ f1_score# --- Object serialization --- #import pickleclass TrainEvaluate:""" This class can be used to train, validate and test sklearn Pipeline objects. """def__init__(self, model: Pipeline, param_grid: dict, target: str, njobs: int=8, verbose: bool=True) ->None:""" model: sklearn Pipeline with the model. param_grid: Dictionary of parameters to search over. target: Name of the column to predict. save_model: Wheter to save the model or not. save_name: Name of the file to save the model. njobs: Number of jobs to run in parallel. verbose: Wheter to print the progress or not. Initialize the class with the model, param_grid, and target variable. """self.model = modelself.param_grid = param_gridself.target = targetself.njobs = njobsself.verbose = verbosepassdef _validation_split(self, df: pd.DataFrame) ->tuple:""" df: Pandas DataFrame with the data. Split the data into train and validation sets. """ y = df[self.target] X = df.drop(self.target, axis=1) X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.25, random_state=42 )return (X_train, X_val, y_train, y_val)def _grid_search(self, X_train: pd.DataFrame, y_train: Union[pd.DataFrame, pd.Series]) -> GridSearchCV:""" X_train: Pandas DataFrame with the training data. y_train: Pandas Series with the training target. """ skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) grid_search = GridSearchCV( estimator=self.model, param_grid=self.param_grid, scoring="roc_auc", n_jobs=self.njobs, cv=skf, verbose=3 ) grid_search = grid_search.fit(X_train, y_train)return grid_searchdef _profit(self, y_true: Union[np.ndarray, pd.DataFrame, pd.Series], y_pred: Union[np.ndarray, pd.DataFrame, pd.Series]) ->float:""" y_true: Pandas Series with the true target. y_pred: Pandas Series with the predicted target. Calculate the profit metric of the model. """ tp = np.sum((y_pred ==1) & (y_true ==1)) fp = np.sum((y_pred ==1) & (y_true ==0)) n =len(y_true) profit = (90* tp -10* fp)return profitdef _threshold_tuning(self, X_val: pd.DataFrame, y_val: Union[pd.DataFrame, pd.Series]) ->float:""" X_val: Pandas DataFrame with the validation data. y_val: Pandas Series with the validation target. Find the threshold that maximizes the profit metric. """ y_proba =self.best_model_.predict_proba(X_val)[:, 1]def profit_treshold(x: float) ->float:""" x: Threshold to test. Returns negative of the profit metric. """ y_pred = (y_proba >= x).astype(int) scalar =-self._profit(y_val, y_pred)return scalar threshold = minimize_scalar( profit_treshold, bounds=(0, 1), method="bounded" )self.threshold = threshold.xreturn threshold.xdef fit(self, df: pd.DataFrame) ->None:""" df: Pandas DataFrame with the data. path: Path to a fitted model. Splits data between train and validation, performs GridSearchCV, adjusts the threshold based on profit metric on the validation set, and fits the model on the original data. """ifself.verbose:print("Splitting data into train and validation sets...") X_train, X_val, y_train, y_val =self._validation_split(df)ifself.verbose:print("Done!")print("Performing GridSearchCV...")self.best_model_ =self._grid_search(X_train, y_train).best_estimator_ifself.verbose:print("Done!")print("Adjusting threshold based on validation set...")self.threshold =self._threshold_tuning(X_val, y_val)ifself.verbose:print("Done!")print("Fitting model on the whole dataset...")self.best_model_ =self.best_model_.fit(df.drop(self.target, axis=1), df[self.target])ifself.verbose:print("Done!")returnselfdef predict_proba(self, df: pd.DataFrame) -> np.ndarray:""" df: Pandas DataFrame with the data. Predicts the target variable using the best model. """returnself.best_model_.predict_proba(df)[:, 1]def predict(self, df: pd.DataFrame) -> np.ndarray:""" df: Pandas DataFrame with the data. Predicts the target variable using the best model and the threshold. """ y_proba =self.predict_proba(df) y_pred = (y_proba >=self.threshold).astype(int)return y_preddef evaluate(self, df: pd.DataFrame) ->dict:""" df: Pandas DataFrame with the test data. Evaluates the model on the data. """ X_test = df.drop(self.target, axis=1) y_true = df[self.target] y_proba =self.predict_proba(X_test) y_pred =self.predict(X_test) tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()self.business_metrics = {"Profit (Total)": tp *90- fp *10,"Profit (per Customer)": (tp *90- fp *10) /len(y_true),"True Positive Profit (Total)": tp *90,"True Positive Profit (per Customer)": tp *90/len(y_true),"False Positive Loss (Total)": fp *10,"False Positive Loss (per Customer)": fp *10/len(y_true),"False Negative Potential Profit Loss (Total)": fn *90,"False Negative Potential Profit Loss (per Customer)": fn *90/len(y_true),"True Negative Loss Prevention (Total)": tn *10,"True Negative Loss Prevention (per Customer)": tn *10/len(y_true) }self.classification_metrics = {"Classification Threshold": self.threshold,"ROC AUC": roc_auc_score(y_true, y_proba),"Precision": precision_score(y_true, y_pred),"Recall": recall_score(y_true, y_pred),"F1": f1_score(y_true, y_pred),"Accuracy": accuracy_score(y_true, y_pred) }returnselfdef _predict_profit(self, model, X: pd.DataFrame, y: pd.Series) ->float:""" X: Pandas DataFrame with the data. y: Pandas Series with the target. Predicts the profit metric using the best model and custom threshold. """ y_pred = model.predict(X)returnself._profit(y, y_pred)def get_feature_importances(self, df: pd.DataFrame) -> pd.DataFrame:""" df: Pandas DataFrame with the data. Implements permutation feature importances on the data using the best model and custom threshold. """ X = df.drop(self.target, axis=1) X =self.best_model_.steps[0][1].transform(X) y = df[self.target] result = permutation_importance(self.best_model_.steps[1][1], X, y, scoring=self._predict_profit, n_repeats=30, random_state=42, n_jobs=self.njobs ) feature_importances = pd.DataFrame({"Feature": X.columns,"Importance": result.importances_mean })self.feature_importances = feature_importances.sort_values("Importance", ascending=False)returnself.feature_importancesdef rank_customers(self, df: pd.DataFrame) -> pd.Series:""" df: Pandas DataFrame with the data. Ranks the customers by their probability of insatisfaction. """ df_ = df.copy() X = df_.drop(self.target, axis=1) y = df_[self.target]def apply_rank(x: float) ->int:""" x: Probability of insatisfaction. Applies the rank (1 to 5) to the probability of insatisfaction. """ thresholds = [c *self.threshold /4for c inrange(5)][::-1]for rank, threshold inenumerate(thresholds):if x >= threshold:return rank +1return5 df_["rank"] =self.predict_proba(X)return df_["rank"].apply(apply_rank)def build_model(path: str=None, train_df: pd.DataFrame =None, model: Pipeline =None, param_grid: dict=None, target: str=None, njobs: int=8, verbose: bool=True) -> TrainEvaluate:""" path: Path to a fitted model. train_df: Pandas DataFrame with the training data. model: sklearn Pipeline with the model. param_grid: Dictionary of parameters to search over. target: Name of the column to predict. njobs: Number of jobs to run in parallel. verbose: Wheter to print the progress or not. Builds a TrainEvaluate object. """ train_evaluate = TrainEvaluate(model, param_grid, target, njobs, verbose) train_evaluate = train_evaluate.fit(train_df)withopen(path, "wb") as f: pickle.dump(train_evaluate, f)return train_evaluate
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Percebe-se que foi adicionado um passo para remover variáveis quase constantes. Por mais que pudessem agregar poder de discriminação, inflar o modelo com dados esparsos – além de ser custoso computacionalmente – obriga que o número de variáveis sorteadas por split seja aumentado para que o modelo consiga selecionar variáveis relevantes, o que pode levar a overfitting.
Mesmo assim, o modelo ainda precisou de um número muito alto de variáveis aleatórias para conseguir selecionar as relevantes, o que pode ter prejudicado sua performance no conjunto de testes. Numa futura iteração deste estudo, o thresh do descartador de colunas poderia ser testado junto com os demais hiperparâmetros – outra vantagem de trabalhar com transformadores customizados.
Com número de estimadores fixado em 500 e class_weight em balanced (para arcar com o desbalanceamento), foram testados os seguintes hiperparâmetros:
O segundo modelo testado foi a implementação do Gradient Boosting de árvores baseado em histogramas do sklearn. Essa implementação é inspirada no LightGBM e foi escolhida apenas por consistência.
Mostrar/esconder código
withopen("models/hgb.pkl", "rb") as f: hgb = pickle.load(f)hgb.best_model_
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
O modelo campeão é aquele que maximiza o lucro resultante da campanha, que é o objetivo do projeto. Assim, o modelo campeão é o HGB. A malha de hiperparâmetros passada para o HBG foi muito maior (11 vezes mais combinações), de modo que a comparação poderia ser considerada injusta. Contudo, é preciso considerar também o custo computacionalde cada modelo. Com o HGB discretizando as variáveis numéricas, a quantidade de splits testados é drasticamente reduzida, o que acelera o treinamento. Num cenário real onde treinar os modelos também acarreta custo, o HGB é ainda mais vantajoso.
Avaliação das features do modelo campeão
Todas as features positivas
Como se pode perceber, foram poucas as variáveis que contribuíram positivamente para o modelo. Uma próxima etapa no desenvolvimento do modelo antes da implementação seria utilizar apenas estas variáveis, eliminando ruído e reduzindo o custo computacional envolvido. O modelo de Random Forest principalmente se beneficiaria muito desta redução, dada a sua natureza aleatória de sortear variáveis em cada split de cada árvore.
Percebe-se que de modo geral as features criadas contribuíram positivamente para o modelo, mostrando a importância da análise exploratória para o desenvolvimento do modelo mesmo sem conhecimento do que cada variável original representa.
Algumas variáveis criadas se mostraram ruidosas, indicando que o modelo pode ter sido sobreajustado. Este comportamento também se repetiu em diversas variáveis originais (vide o documento original da classificação). Reduzir o número de features poderia ser uma solução, mas também seria interessante testar uma malha de hiperparâmetros com penalizações mais severas, principalmente considerando que a maior penalização testada foi a selecionada no GridSearchCV.
Usando o mesmo modelo de HGB carregado anteriormente, os clientes foram rankeados com base na probabilidade de insatisfação. O corte de classificação do rank 1 é o mesmo do modelo em si, enquanto os demais são steps iguais entre 0 e o corte original.
Antes realizar qualquer tipo de análise, recuperamos o modelo de classificação para atribuir o lucro de cada cliente da base de testes, mas concatenamos o conjunto de testes com o conjunto de treino, preenchendo o lucro de cada cliente deste com np.nan para que forneçam volume nos dados mas não causem viés na análise.
Como o conjunto é dado por variáveis numéricas, os algorítmos usados serão agrupamentos baseados em distância. Sem conhecimento do que realmente é cada variável, não é possível tomar decisões baseadas em conhecimento da área.
A primeira etapa para determinar o processamento dos dados e sua preparação para a clusterização foi recuperar o pipeline de processamento de dados usado na classificação e aplicá-lo novamente.
Mostrar/esconder código
pdf = df.drop(["TARGET","predicted","profit","origin"], axis=1)prep = build_prep()[:-2].fit(pdf)pdf = prep.transform(pdf)prep
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Em seguida, foi avaliada a esparsidade das features. A tabela abaixo mostra a quantidade de colunas em cada faixa de concentração (%) de valores iguais a zero.
Como a faixa de 40% não possui nenhuma variável, este valor foi definido para reduzir o número de variáveis do modelo, permitindo uma análise mais minusciosa das restantes.
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
É importante ressaltar que embora a primeira componente explique muito bem a variabilidade do conjunto, foi necessário manter 9 componentes principais para que a variância explicada chegasse a 80% (valor comumente utilizado neste tipo de análise). Assim, a visualização dos dados não é tão simples.
Abaixo, a distribuição das 3 componentes principais:
Datas as distribuições das 3 componentes principais, optou-se por utilizar o K-Means como algoritmo de agrupamento. Abaixo, temos o gráfico de cotovelo para determinar o número de clusters.
Mostrar/esconder código
ssd = []for num_clusters inrange(1, 31): kmeans = KMeans(n_clusters=num_clusters, random_state=42, n_init='auto') kmeans.fit(tdf[[i for i inrange(0,10)]]) ssd.append(kmeans.inertia_)plt.figure(figsize=(10,6))plt.plot(range(1, 31), ssd, marker='o', linestyle='--')plt.xlabel('Number of Clusters')plt.ylabel('Sum of Squared Distances')plt.title('Elbow Method For Optimal Number of Clusters')plt.show()
Este estudo foi realizado com o objetivo de maximizar o lucro de uma campanha de retenção. Para isso, foram realizadas análises exploratórias, desenvolvidos modelos de classificação e rankeamento e realizada uma análise de agrupamentos naturais. Com conhecimento de mercado, seria possível usar os clusters para direcionar ações específicas para cada grupo de clientes, usando o modelo de classificação para determinar quais estariam de fato insatisfeitos. Nenhum modelo aqui estaria pronto para a produção e outros passos (como feature selection mais rigoroso e análise exploratória mais cuidadosa) poderiam ser adicionados para garantir a robustez do modelo final. Foi possível criar um modelo de classificação lucrativo e encontrar agrupamentos bem definidos, ainda criando margens de observação para clientes possivelmente insatisfeitos com base no rankeamento. Agradeço os envolvidos no processo pela oportunidade de participar do Data Masters e espero que este estudo gere boas discussões acerca do tema.